<?php
/**
 * Mechanism
 *
 * LICENSE
 *
 * Copyright (c) 2011, Sergey A. Ivonchik <BreatheInMyVoid@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following disclaimer
 *   in the documentation and/or other materials provided with the
 *   distribution.
 * * Neither the name of the  nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @package Mechanism
 * @license http://www.opensource.org/licenses/bsd-license
 * @author Sergey A. Ivonchik <BreatheInMyVoid@gmail.com>
 * @copyright Copyright (c) 2011 Sergey A. Ivonchik
 */

namespace Mechanism\Router\Route {
	/**
 	 * Abstract class of route
 	 *
	 * This is abstract class for all routes.
	 *
 	 * @package Mechanism\Router\Route
 	 * @author BreatheInMyVoid <BreatheInMyVoid@gmail.com>
 	 */
	abstract class Route implements IRoute {
		/**
		 * Separartor rule 
		 * 
		 * @var string SEPARATOR_RULE 
		 */
		const SEPARATOR_RULE = '{s}';
		
		/**
		 * Static variable rule
		 * 
		 * @var string STATIC_VAR_RULE
		 */
		const STATIC_VAR_RULE = '<>';
		
		/**
		 * Dynamic variable rule
		 * 
		 * @var string DYNAMIC_VAR_RULE
		 */
		const DYNAMIC_VAR_RULE = '[]';

		/**
		 * Regular expression rule
		 * 
		 * @var string REGEX_VAR_RULE
		 */
		const REGEX_VAR_RULE = '()';
		
		/**
		 * Sprcial parameter rule 
		 * 
		 * @var string SPECPARAM_VAR_RULE
		 */
		const SPECPARAM_VAR_RULE = '{}';
		
		/**
		 * Default separator for route parts
		 *
		 * Default separator must be declarated
		 * in child class like regex rule.
		 *
		 * @var string $separator
		 */
		protected $separator = '';

		/**
		 * Default regex rule for dynamic variables
		 *
		 * @see $this->__constructor( )
		 * @see $this->setUpDefaultRule( )
		 * @var string $defaultRule
		 */
		protected $defaultRule = '';

		/**
		 * Raw exploded route rule
		 *
		 * @var array $route
		 */
		protected $route = array( );

		/**
		 * Defaults route variables
		 *
		 * @var array $defaults
		 */
		protected $defaults = array( );

		/**
		 * Parameters of a route
		 *
		 * @var array $params
		 */
		protected $params = array( );

		/**
		 * Is require to have parameters
		 * 
		 * @var bool $isRequireParams
		 */
		protected $isRequireParams = false;
		
		/**
		 * Constructor
		 *
		 * TODO: fix examples 
		 * 
		 * Constructor takes rule of a route using
		 * simple rules for making pattern. This constructor
		 * explodes pattern into array by $part => $rule
		 * and saves it into $this->route with it's sequance.
		 * Rule can not stsrts with separator. It beacause of
		 * we do not whant to collding with historical url rules
		 * and let to make urls too confused. You can't make route
		 * like http://example.com controller action, it must be
		 * http://example.com/controller action if you want so.
		 * F.e. / - separate host from path, ? - separate query 
		 * from path and etc.
		 *
		 * Route build rules:
		 *     <> - static parts
		 *     [] - named dynamic variable
		 *     () - regex rules
		 *     {} - route special parameters
		 * 
		 * {s} - separator
		 * {h:host} - hostname
		 * 
		 * Examples:
		 * // ru/news/politics/show
		 * $route = new Router\Route\Uri( '[lang]{s}[module]{s}[controller]{s}[action]' );
		 *
		 * // news/politics/show
		 * $route = new Router\Route\Uri( '<news>{s}[controller]{s}[action]' );
		 *
		 * // game/paly with ball
		 * $route = new Router\Route\Uri( '<game>[action]{s}with{s}[controller]' );
		 * $route->setSeparator( ' ' );
		 *
		 * // game/paly blahblah ball
		 * $route = new Router\Route\Uri( '<game>[action]{s}([a-zA-Z]+){s}[controller]' );
		 * $route->setSeparator( ' ' );
		 *
		 * @param string $route
		 * @param array $defaults
		 */
		public function __construct( $route, array $defaults = null ) {
			/* Check separator (for childern classes) */
			if  ( '' === $this->separator ) {
				throw new Exception\Runtime( 'Separator is null or not setted' );
			}

			/* Check that route rule do not starts from separator */
			if ( self::SEPARATOR_RULE === substr( $route, 0, strlen( self::SEPARATOR_RULE ) ) ) {
				throw new Exception\Invalid( 'Route rule can not starts from separator' );
			}

			/* Setting up default rule */
			$this->setUpDefaultRule( $this->separator );
			
			/* Create raw array for regex rule assemblig */
			$raw = explode( self::SEPARATOR_RULE, ( string ) $route );
			$rawLength = count( $raw );

			/* Create route array */
			for ( $i = 0; $i < $rawLength; ++$i ) {
				$stingEnds = $this->getStringEnds( $raw[$i] );
				
				/* Find out dynamic variables */
				if ( self::DYNAMIC_VAR_RULE === $stingEnds ) {
					 $this->route[$raw[$i]] = $this->defaultRule;
				/* Find out regex rules */
				} else if ( self::REGEX_VAR_RULE === $stingEnds ) {
					$this->route[$raw[$i]] = substr( $raw[$i], 1, -1 );
				/* Find out special parameters {name:value} exclude separator */
				} else if ( self::SPECPARAM_VAR_RULE === $stingEnds ) {
					$params = explode( ':', substr( $raw[$i], 1, -1 ) );
					$this->route['{' . $params[0] . '}'] = $params[1];
				/* Find out static parts */
				} else if ( self::STATIC_VAR_RULE === $stingEnds ) {
					$this->route[$raw[$i]] = substr( $raw[$i], 1, -1 );
				}
			}

			/* Setup defaults */
			if ( null !== $defaults ) {
				foreach( $defaults as $name => $value ) {
					$this->setDefault( $name, $value );
				}
			}
		}

		/**
		 * Set parameter of a route
		 *
		 * This method sets special parameter for
		 * a route f.e. module, controller, action
		 * and etc.
		 *
		 * @param string $name
		 * @param string $value
		 * @return Mechanism\Router\Route $this
		 */
		public function set( $name, $value ) {
			$this->params[$name] = $value;

			return $this;
		}

		/**
		 * Get parameter of a route
		 *
		 * This method gets special parameter of a route
		 * or all parameters if called without arguments.
		 * Using this method you can get route parameters
		 * after route matching. Before matching it returns
		 * parameters which you setted manually.
		 * This is a "main" method of a routes. Basicly
		 * this method should return what you need after routing
		 * and its value.
		 *     $route = array(
		 *         'module' => 'main',
		 *         'controller' => 'index',
		 *		   'action' => 'index'
		 *	   );
		 *
		 *     $route = array(
		 *         'module' => 'main',
		 *         'page' => 'show',
		 *	   );
		 * Using this system we can return any parameters
		 * you want. But you must think about returning data 
		 * because some objects expects some parameters from 
		 * this method. F.e. Front controller expects module,
		 * controller and action, page conroller expects page.
		 * This method it is alternative to the countless 
		 * generation of getters and setters.
		 *
		 *
		 * @param string $name = null
		 * @return string $this->params[$name]
		 * @return array $this->params
		 */
		public function get( $name = null ) {
			if ( null === $name ) {
				return $this->params;
			} else {
				return array_key_exists( $name, $this->params ) ?
					$this->params[$name] : null;
			}
		}

		/**
		 * Set default value for route parameters
		 *
		 * This method sets default values for
		 * route parameters which declarated.
		 * This parameters will be used if route
		 * parameter value will not be found in
		 * matching path but was declarated in pattern.
		 *
		 * @param string $name
		 * @param mixed $value
		 * @return Mechanism\Router\Route $this
		 */
		public function setDefault( $name, $value ) {
			$this->defaults[$name] = $value;

			return $this;
		}

		/**
		 * Get default value of route parameter
		 * 
		 * @param string $name
		 * @return string $this->defaults[$name]
		 * @return array $this->defaults
		 */
		public function getDefault( $name = null ) {
			if ( null === $name ) {
				return $this->defaults;
			} else {
				return array_key_exists( $name, $this->defaults ) ?
					$this->defaults[$name] : null;
			}
		}

		/**
		 * Set rule
		 * 
		 * This method changes default regex rule 
		 * for route dynamic parameters.
		 *
		 * @throws Mechanism\Router\Route\Exception\Runtime
		 * @param string $param
		 * @param string $rule
		 * @return Mechanism\Router\Route $this
		 */
		public function setRule( $param, $rule ) {
			$dynamicVarRule = self::DYNAMIC_VAR_RULE;
			$param = $dynamicVarRule[0] . $param . $dynamicVarRule[1];
			
			if ( array_key_exists( $param, $this->route ) ) {
				$this->route[$param] = ( string ) $rule;

				return $this;
			} else {
				throw new Exception\Runtime(
					'There is no ' . $param . ' part of route for rule ' . $rule
				);
			}
		}

		/**
		 * Get rule
		 *
		 * This method gets rule for route dynamic
		 * parameter and returns it or null if
		 * parameter not exists.
		 *
		 * @param string $param
		 * @return string $this->route[$param]
		 * @return null
		 */
		public function getRule( $param ) {
			$dynamicVarRule = self::DYNAMIC_VAR_RULE;
			$param = $dynamicVarRule[0] . $param . $dynamicVarRule[1];			

			return array_key_exists( $param, $this->route ) ?
				$this->route[$param] : null;
		}

		/**
		 * Set separator
		 *
		 * This method sets separator for route and it's
		 * rule for regex pattern
		 *
		 * @param string $separator
		 * @return Mechanism\Router\Route $this
		 */
		public function setSeparator( $separator ) {
			/* Must be escaped for regexp */
			$escaped = array( '/', '#', ' ', '|', '.' );

			if ( in_array( $separator, $escaped ) ) {
				$this->separator = '\\' . $separator . '?';
			} else {
				$this->separator = $separator . '?';
			}

			/* Change default rule */
			$this->setUpDefaultRule( $this->separator );
			
			return $this;
		}

		/**
		 * Return raw separator
		 * 
		 * This method return raw separator.
		 * 
		 * @return string $separator
		 */
		public function getSeparator( ) {
			if ( '\\' === $this->separator[0] ) {
				return substr( $this->separator, 1, -1 );
			} else {
				return substr( $this->separator, 0, -1 );
			}
		}

		/**
		 * Require parameters
		 * 
		 * Turning this flag on, user specify that
		 * params from route MUST be in matching url
		 * and if it not, $this->match( ) returns false.
		 * 
		 * @see UriTest.php
		 * @see $this->haveInUriParams( $flag = true )
		 * @param bool $flag
		 * @return Mechanism\Router\Route $this
		 */
		public function requireParams( $flag = true ) {
			$this->isRequireParams = $flag;
			return $this;
		}
		
		/**
		 * Get string ends
		 * 
		 * Utility method for checking constructor
		 * route rules. This method also checks
		 * all default building variables rules and throws
		 * Invalid exception if user passed wrong rule.
		 * F.e. if user passed '[controller{s}[action)
		 * 
		 * @var string $string
		 * @return string $ends
		 * @throw Mechanism\Router\Route\Exception\Invalid
		 */
		protected function getStringEnds( $string ) {
			$ends = '';
			$rules = array( self::SEPARATOR_RULE,
							self::DYNAMIC_VAR_RULE,
							self::REGEX_VAR_RULE,
							self::SPECPARAM_VAR_RULE,
							self::STATIC_VAR_RULE );
							
			$ends = $string[0] . $string[strlen( $string ) - 1];
			
			if ( in_array( $ends, $rules ) ) {
				return $ends;
			} else {
				throw new Exception\Invalid( 'Wrong route rule' );
			}
		}
		
		/**
		 * Setup parameters
		 * 
		 * This method finally setups params after matching,
		 * assembling and disassembling url. 
		 * 
		 * @param array $disassembledUri
		 * @param array $route
		 * @return void
		 */
		protected function setUpParams( array $disassembledUri, array $route ) {
			/* Preincrement for little improvment of speed ;) */
			$i = -1;
			
			foreach( $route as $param => $value ) {
				++$i;

				$stringEnds = $this->getStringEnds( $param );
				
				/* Pass off static parts and regex parts */
				if ( ( self::STATIC_VAR_RULE === $stringEnds ) 		||
					 ( self::REGEX_VAR_RULE === $stringEnds )  		||
					 ( self::SPECPARAM_VAR_RULE === $stringEnds ) ) {
					continue;
				} else {
					$p = null;
					$param = substr( $param, 1, -1 );
					
					/* Is parameter value exists in url? */
					if ( isset( $disassembledUri[$i] ) && 
						 !empty( $disassembledUri[$i] ) ) {
						$p = $disassembledUri[$i];
					/* Is parameter value exists in defaults? */
					} else if ( isset( $this->defaults[$param] ) && 
								!empty( $this->defaults[$param] ) ) {
						$p = $this->defaults[$param];
					} else {
					/* If parameter unexsists in both we do not set in up */
						continue;
					}
					
					$this->set( $param, $p );
				}
			}			
		}
		
		/**
		 * Setup default rule
		 *
		 * This method should setup regex pattern for
		 * default rule of route dynamic variables.
		 * It calls in constrictor and when you change
		 * separator. 
		 *	
		 * @see $this->__constructor( $path )
		 * @see $this->setSeparator( $separator )
		 * @param string $separator
		 */
		protected function setUpDefaultRule( $separator ) {
			/* Setup rule riddig off metasymbol from separator */
			$this->defaultRule = '[^\s' . substr( $separator, 0, -1 ) . ']*';
		}
		
		/** 
		 * Have params in matched url?
		 * 
		 * If user specify mandority params in url using
		 * $this->requireParams( ) method we use this one
		 * to check do url have it or not.
		 * 
		 * @see $this->requireParams( $flag = true )
		 * @see $this->match( $path )
		 * @param string $path
		 * @return bool $flag
		 */
		protected function haveInUrlParams( $path ) {	
			$flag = false;
			$params = explode( $this->getSeparator( ), ( string ) $path );
			
			foreach( $params as $value ) {
				if ( ( "0" === $value ) || !empty( $value ) ) {
					$flag = true;
				} 
			}
			
			return $flag;
		}
	}
}